/*
 * Archive.h
 *
 *  Created on: 2011/07/02
 *      Author: naoki
 */

#ifndef INCLUDED_NTENGINE_FILEIO_ARCHIVE_H
#define INCLUDED_NTENGINE_FILEIO_ARCHIVE_H

#include "NTEngine/Base/Compressor.h"
#include "NTEngine/Base/HashMap.h"
#include <android/asset_manager.h>
#include <fstream>

namespace NTEngine {
namespace FileIO {
using namespace std;
/// ファイル操作インターフェース
class InputFileStream {
public:
	/// seek開始位置enum
	enum SeekWhence {
		SEEK_WHENCE_BEG, // 先頭から
		SEEK_WHENCE_CUR, // 現在の位置から
		SEEK_WHENCE_END, // 終端から
	};
	virtual ~InputFileStream() { }
	virtual void seek( int offset, SeekWhence ) = 0;
	virtual int read( char* out, int len ) = 0;
	virtual bool good() const = 0;
	virtual int size() = 0;
};
/// ifstream使用
class StrageStream : public InputFileStream {
public:
	StrageStream( const char* name ) : mStream( name, ifstream::binary ) {
	}
	~StrageStream() {
	}
	// @Override
	void seek( int offset, SeekWhence seekWhence ) {
		ios::seekdir whence;
		switch ( seekWhence ) {
		case SEEK_WHENCE_BEG: whence = ifstream::beg; break;
		case SEEK_WHENCE_CUR: whence = ifstream::cur; break;
		case SEEK_WHENCE_END: whence = ifstream::end; break;
		}
		mStream.seekg( offset, whence );
	}
	// @Override
	int read( char* out, int len ) {
		mStream.read( out, len );
		return mStream.gcount();
	}
	// @Override
	bool good() const {
		return mStream.good();
	}
	// このサイズ取得のあとは適宜seekし直すこと
	// @Override
	int size() {
		return mStream.seekg( 0, ifstream::end ).tellg();
	}
private:
	ifstream mStream;
};
/// Asset使用
class AssetStream : public InputFileStream {
public:
	AssetStream( const char* name, AAssetManager* assetManager ) : mAsset( 0 ) {
		mAsset = AAssetManager_open( assetManager, name, AASSET_MODE_UNKNOWN );
	}
	~AssetStream() {
		if ( mAsset ) {
			AAsset_close( mAsset );
			mAsset = 0;
		}
	}
	// @Override
	void seek( int offset, SeekWhence seekWhence ) {
		int whence;
		switch ( seekWhence ) {
		case SEEK_WHENCE_BEG: whence = SEEK_SET; break;
		case SEEK_WHENCE_CUR: whence = SEEK_CUR; break;
		case SEEK_WHENCE_END: whence = SEEK_END; break;
		}
		AAsset_seek( mAsset, offset, whence );
	}
	// @Override
	int read( char* out, int len ) {
		return AAsset_read( mAsset, out, len );
	}
	// @Override
	bool good() const {
		return ( mAsset != 0 );
	}
	// @Override
	int size() {
		return AAsset_getLength( mAsset );
	}
private:
	AAsset* mAsset;
};

/**
 * @brief アーカイバ解凍クラス
 */
class Archive {
public:
	Archive( const char* name, AAssetManager* assetManager = 0 ) :
	mFileNumber( 0 ),
	mBlockSize( 0 ),
	mStream( 0 ),
	mDummy( false ) {
		if ( !name ) {
			mDummy = true; //ダミーアーカイブ。直接ファイルから読み込みます。
			return;
		}
		//ファイルを開けてメンバに持っておく。
		if ( assetManager ) {
			mStream = NEW AssetStream( name, assetManager );
		} else {
//			setlocale( LC_ALL, "" ); //これがないと日本語ファイル名を受け付けない
			mStream = NEW StrageStream( name );
		}
		if ( !mStream->good() ) {
			cout << "can't open archive: " << name << endl;
			SAFE_DELETE( mStream );
			return;
		}
		//末尾から4バイト前へ移動
		//ファイルサイズを一回測ろうか。エラーチェックのためにな。
		int fileSize = mStream->size();
		if ( fileSize < 8 ) { //まずありえん
			cout << "archive size is illegal ( too small ): " << name << endl;
			SAFE_DELETE( mStream );
			return;
		}
		mStream->seek( -8, InputFileStream::SEEK_WHENCE_END );
		mBlockSize = getUnsigned();
		int  tableBegin = getUnsigned() * mBlockSize;
		//テーブル先頭へ移動
		if ( tableBegin + 12 >= fileSize ) { //ありえん
			cout << "archive size is illegal ( wrong table ): " << name << endl;
			SAFE_DELETE( mStream );
			return;
		}
		mStream->seek( tableBegin, InputFileStream::SEEK_WHENCE_BEG );
		//4バイト読むとファイル数
		mFileNumber = getUnsigned();
		if ( fileSize < mFileNumber * 16 ) { //ありえん
			cout << "archive size is illegal ( wrong file number ): " << name << endl;
			SAFE_DELETE( mStream );
			return;
		}
		//ハッシュマップサイズ確保
		mEntries.setCapacity( mFileNumber );
		//後はループで回しながら読んでいく。
		for ( int i = 0; i < mFileNumber; ++i ) {
			Entry e;
			e.mPosition = getUnsigned();
			e.mSize = getUnsigned();
			e.mOriginalSize = getUnsigned();
			e.mNecessaryBufferSize = getUnsigned();
			int nameLength = getUnsigned();
			//名前は一旦一時配列にいれる。すぐdeleteするが。
			char* name = NEW char[ nameLength + 1 ]; //終端NULLで+1
			mStream->read( name, nameLength );
			name[ nameLength ] = '\0'; //終端NULL
			//mEntriesはmap< char*, Entry >
			mEntries.add( name, e ); //mapに格納
			//情報を吐き出してみよう。正しいか？
			SAFE_DELETE_ARRAY( name );
		}
	}
	~Archive() {
		SAFE_DELETE( mStream );
	}
	void open(
		InputFileStream** streamOut,
		int* entryIndex,
		const char* name,
		AAssetManager* assetManager = 0 )
	{
		if ( mDummy ) {
			*entryIndex = -1;
			if ( assetManager ) {
				*streamOut = NEW AssetStream( name, assetManager );
			} else {
//				setlocale( LC_ALL, "" ); //これがないと日本語ファイル名を受け付けない
				*streamOut = NEW StrageStream( name );
			}
			if ( !( ( *streamOut )->good() ) ) {
				SAFE_DELETE( *streamOut ); //ない
			}
		} else {
			if ( mFileNumber == 0 ) { //ないアーカイブなら常に見つからない
				*entryIndex = -1;
				*streamOut = 0;
			} else {
				//スラッシュとバックスラッシュの両方を受け付けるためにバックスラッシュ化
				string tName = name;
				for ( size_t i = 0; i < tName.size(); ++i ) {
					if ( tName[ i ] == '/' ){
						tName[ i ] = '\\';
					}
				}
				int it = mEntries.find( tName );
				if ( !mEntries.isEnd( it ) ) {
					*entryIndex = it;
					*streamOut = mStream;
					const Entry& e = *mEntries.value( *entryIndex );
					int  pos = e.mPosition * mBlockSize;
					mStream->seek( pos, InputFileStream::SEEK_WHENCE_BEG );
				} else {
					*entryIndex = -1;
					*streamOut = 0;
				}
			}
		}
	}
	void getFileSize( int* readSize, int* originalSize, int entryIndex, InputFileStream* stream ) const {
		if ( mDummy ) {
			int r = stream->size();
			stream->seek( 0, InputFileStream::SEEK_WHENCE_BEG );
			*readSize = *originalSize = r;
		} else {
			HARD_ASSERT( entryIndex >= 0 );
			HARD_ASSERT( stream == mStream );
			const Entry& e = *mEntries.value( entryIndex );
			*readSize = e.mSize;
			*originalSize = e.mOriginalSize;
		}
	}
	void allocate( char** dataOut, int size, int entryIndex ) {
		if ( mDummy ) {
			HARD_ASSERT( entryIndex == -1 );
			*dataOut = NEW char[ size + 1 ]; //親切設計NULL終端。
		} else {
			HARD_ASSERT( entryIndex >= 0 );
			const Entry& e = *mEntries.value( entryIndex );
			int allocSize = max( e.mNecessaryBufferSize, e.mOriginalSize + 1 ); //展開に必要なバッファサイズがあるならそれも書く。
			*dataOut = NEW char[ allocSize ];
		}
	}
	void read(
		bool* errorOut,
		char* data,
		int size,
		int entryIndex,
		InputFileStream* stream )
	{
		bool compressed = false;
		int originalSize = size;
		int necessaryBufferSize = size;
		if ( !mDummy ) {
			HARD_ASSERT( stream == mStream );
			HARD_ASSERT( entryIndex >= 0 );
			const Entry& e = *mEntries.value( entryIndex );
			HARD_ASSERT( e.mSize == size );
			if ( e.mOriginalSize != size ) {
				compressed = true;
				originalSize = e.mOriginalSize;
				necessaryBufferSize = e.mNecessaryBufferSize;
			}
		}
		int readOffset = 0;
		if ( compressed ) {
			readOffset = necessaryBufferSize - size; //後ろにロード。展開に余分に必要な場合があるのでoriginalSizeではない。
		}
		if ( stream->read( data + readOffset, size ) != size ) { //エラーか
			*errorOut = true;
			return;
		}
		if ( compressed ) {
			int outSize;
			Compressor::decompress(
				data,
				&outSize,
				data + readOffset,
				size );
			if ( outSize != originalSize ) {
				cout << "Decompression Failed!" << endl;
				*errorOut = true;
			}
		}
	}
	void close(	InputFileStream** stream ) {
		if ( mDummy ) {
			SAFE_DELETE( *stream );
		} else {
			*stream = 0;
		}
	}
private:
	class Entry{
	public:
		unsigned mPosition;
		int mSize;
		int mOriginalSize;
		int mNecessaryBufferSize;
	};
	unsigned getUnsigned(){
		unsigned char buffer[ 4 ];
		mStream->read( reinterpret_cast< char* >( buffer ), 4 );
		unsigned r = buffer[ 0 ];
		r |= ( buffer[ 1 ] << 8 );
		r |= ( buffer[ 2 ] << 16 );
		r |= ( buffer[ 3 ] << 24 );
		return r;
	}
	int mFileNumber;
	unsigned mBlockSize;
	InputFileStream* mStream;
	HashMap< string, Entry > mEntries;
	bool mDummy;
};

} //namespace FileIO
} //namespace NTEngine
#endif // INCLUDED_NTENGINE_FILEIO_ARCHIVE_H
